using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace System
{
	/// <summary>
	/// 
	/// </summary>
	internal static class FluentCast
	{
		private static readonly Dictionary<string, MethodInfo> CastOperators = new Dictionary<string, MethodInfo>();

		/// <summary>
		/// Dynamically looks up the appropriate cast operator and calls it.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source">The source.</param>
		/// <returns></returns>
		public static IEnumerable<T> CastTo<T>(this IEnumerable<object> source)
		{
			return (source == null) ? null : source.Select(item => item.CastTo<T>());
		}

		/// <summary>
		/// Dynamically looks up the appropriate cast operator and calls it.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source">The source.</param>
		/// <returns></returns>
		public static T CastTo<T>(this object source)
		{
			Type targetType = typeof(T);
			if (source == null)
			{
				if (!IsNullableType(targetType))
				{
					throw new InvalidOperationException("Cannot convert NULL to a non-nullable type '" + targetType + "'.");
				}
				return default(T);
			}
			Type sourceType = source.GetType();
			if (targetType == sourceType)
			{
				return (T)source;
			}
			MethodInfo castOperator = EnsureCastOperator(sourceType, targetType);
			return (T)castOperator.Invoke(null, new[] {source});
		}

		private static MethodInfo EnsureCastOperator(Type sourceType, Type returnType)
		{
			string key = string.Format("{0} to {1}", sourceType.AssemblyQualifiedName, returnType.AssemblyQualifiedName);
			if (CastOperators.ContainsKey(key))
			{
				return CastOperators[key];
			}

			MethodInfo castOperator =
				//cast defined on source
				sourceType.GetMethods(BindingFlags.Static | BindingFlags.Public)
					.Where(mi => (mi.Name == "op_Implicit") || (mi.Name == "op_Explicit"))
					.Where(mi => mi.ReturnType == returnType)
					.FirstOrDefault() ?? 
				//cast defined on target
				returnType.GetMethods(BindingFlags.Static | BindingFlags.Public)
					.Where(mi => (mi.Name == "op_Implicit") || (mi.Name == "op_Explicit"))
					.Where(mi => mi.GetParameters()[0].ParameterType == sourceType)
					.FirstOrDefault();

			if (castOperator == null)
			{
				throw new InvalidCastException(string.Format("Invalid Cast from {0} to {1}", sourceType, returnType));
			}

			CastOperators.Add(key, castOperator);
			return castOperator;
		}

		private static bool IsNullableType(Type type)
		{
			if (!type.IsValueType) // ref-type
			{
				return true;
			}
			return Nullable.GetUnderlyingType(type) != null;
		}
	}
}